﻿using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
using RabbitMQ.Client;

namespace smp.RabbitMq.Strategies
{
    public class RetryQueueSubscriberStrategy : ISubscriberStrategy
    {
        private readonly int _maxAttempts;
        private readonly int _retrySleepInMilliSeconds;

        public RetryQueueSubscriberStrategy()
        {
            _maxAttempts = 3;
            _retrySleepInMilliSeconds = 300000;
        }

        public RetryQueueSubscriberStrategy(int maxAttempts)
        {
            _maxAttempts = maxAttempts;
            _retrySleepInMilliSeconds = 300000;
        }

        public RetryQueueSubscriberStrategy(int maxRetryAttempts, int retrySleepInMilliSeconds)
        {
            _maxAttempts = maxRetryAttempts;
            _retrySleepInMilliSeconds = retrySleepInMilliSeconds;
        }

        #region Implementation of ISubscriberStrategy

        public StrategyResult HandleMessage(List<ISubscriberHandler> handlers, Envelope envelope)
        {
            var type = Type.GetType(envelope.MessageType);
            var message = JsonConvert.DeserializeObject(envelope.Message, type);
            if (message == null)
            {
                InstancesFactory.Log.Warning("Null or empty message entity recieved, or could not deserialize message {0} to type {1}", envelope.Message, envelope.MessageType);
                return new StrategyResult { Success = true };
            }

            var exceptions = new List<Exception>();
            foreach (var handler in handlers)
            {
                try
                {
                    ExecuteHandler(message, handler);
                }
                catch (Exception ex)
                {
                    exceptions.Add(ex);
                }
            }

            if (exceptions.Count == 0)
                return new StrategyResult {Success = true};

            if(envelope.Attempts >= _maxAttempts - 1)
            {
                foreach(var exception in exceptions)
                {
                    InstancesFactory.Log.Error(exception);
                }
                return new StrategyResult {Success = false};
            }

            try
            {
                ReQueueEnvelope(envelope);
                return new StrategyResult {Success = true};
            }
            catch (Exception ex)
            {
                InstancesFactory.Log.Error(ex, "Failed to requeue message");
                return new StrategyResult {Success = false};
            }
        }

        private void ExecuteHandler(object message, ISubscriberHandler handler)
        {
            var subscriberHandler = (dynamic)handler;
            var messageEntity = (dynamic)message;
            InstancesFactory.Log.Debug("Executing responder handler : {0}", handler.GetType());
            subscriberHandler.Handle(messageEntity);
        }

        private void ReQueueEnvelope(Envelope envelope)
        {
            envelope.Attempts++;

            var configuration = InstancesFactory.SubscriberConfiguration;
            var connectionFactory = SingletonFactory.Instance.GetConnectionFactory(configuration);
            var connection = connectionFactory.CreateConnection();

            InstancesFactory.Log.Debug("Created RabbitMq connection to host : {0} and virtual host : {1}", configuration.Host, configuration.VirtualHost);

            var channel = connection.CreateModel();
            InstancesFactory.Log.Debug("Created RabbitMq Channel on connection on host : {0} and virtual host : {1}", configuration.Host, configuration.VirtualHost);

            var reQueueExchange = configuration.Queue + "_REQUEUE";
            channel.ExchangeDeclare(reQueueExchange, configuration.ExchangeType.ToString(), configuration.DurableExchange, configuration.AutoDeleteExchange, null);
            InstancesFactory.Log.Debug("Declared requeue exchange {0}", reQueueExchange);

            var requeueRoutingKey = configuration.RoutingKey + "_requeue";
            
            if(_retrySleepInMilliSeconds == 0)
            {
                channel.QueueBind(configuration.Queue, reQueueExchange, requeueRoutingKey);
                InstancesFactory.Log.Debug("Bound queue {0} to requeue exchange {1} with routing key {2}", configuration.Queue, reQueueExchange, requeueRoutingKey);
                channel.BasicQos(0, 1, false);
            }
            else
            {
                var reQueueSleepQueue = configuration.Queue + "_REQUEUE_SLEEP";
                var queueArgs = new Dictionary<string, object>
                                {
                                    {"x-dead-letter-exchange", reQueueExchange},
                                    {"x-message-ttl", _retrySleepInMilliSeconds},
                                    {"x-expires", 3600000}
                                };
                channel.QueueDeclare(reQueueSleepQueue, true, false, false, queueArgs);
                channel.QueueBind(reQueueSleepQueue, reQueueExchange, requeueRoutingKey, null);
                InstancesFactory.Log.Debug("Bound queue {0} to requeue exchange {1} with routing key {2}", reQueueSleepQueue, reQueueExchange, requeueRoutingKey);
                channel.BasicQos(0, 1, false);
            }

            PublishRequeue(reQueueExchange, requeueRoutingKey, configuration.DurableExchange, channel, envelope);
        }

        private void PublishRequeue(string exchange, string routingKey, bool durableExchange, IModel channel, Envelope envelope)
        {
            var serializedMessage = JsonConvert.SerializeObject(envelope);
            var packet = Encoding.ASCII.GetBytes(serializedMessage);
            try
            {
                var properties = channel.CreateBasicProperties();
                properties.SetPersistent(durableExchange);
                properties.Priority = 0;
                channel.BasicPublish(exchange, routingKey, properties, packet);
                InstancesFactory.Log.Info("Re-Published message : {1} to exchange : {0}", exchange, serializedMessage);
            }
            catch (Exception ex)
            {
                var error = new Exception(string.Format("An error was throws when trying to send message {0} on exchange {1}", serializedMessage, exchange), ex);
                InstancesFactory.Log.Error(error);
                throw error;
            }
        }

        #endregion
    }
}
